fix(cli): a written inline I18nLabel map is no longer reported as an untranslated string - #15980
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…line-i18nlabel-not-unauthored
📓 Docs Drift Check31 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 135 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 62af40efa39c08fd72b78b28e850c6b24fefc6af && git checkout 62af40efa39c08fd72b78b28e850c6b24fefc6af
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6615a024c33a9204a7a88273aa40b9ee107f78e3 a24e088e7a3544d2344dbd5abe2c0481cb57841f && git checkout -B drift-repro 6615a024c33a9204a7a88273aa40b9ee107f78e3 && git merge --no-ff a24e088e7a3544d2344dbd5abe2c0481cb57841f
node scripts/docs-audit/affected-docs.mjs --json 6615a024c33a9204a7a88273aa40b9ee107f78e3 |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
|
Standing down on the red What failed
The readingThe self-test's verdict is a pure function of the gate runner, not of the tree. Its fixture writes a stub Reproduced by shimming
The two failing runs are byte-identical to each other and carry CI's own summary line verbatim. Also worth recording, since it invites a wrong read: ConsequenceThe defect can only produce a false RED, never a false green — a failing launcher makes every artifact look stale, and stale is the refusing verdict. So it costs a triage cycle on an innocent PR rather than letting anything through. #15990 carries the mechanism, the 2x2, and three candidate directions, none of them implemented here. Re-running this check should clear it whenever the launcher resolves; that call is the PM's, and the one permitted re-run is unspent. Generated by Claude Code |
…line-i18nlabel-not-unauthored
Closes #14749
Maintainer ruling of 2026-09-03 on this card, Q2 = B1 and Q3 = C3: the coverage gate stops recording a fully-written inline
I18nLabelmap under the same diagnostic as a key the author never wrote, and ⛔ no key scheme is built from a node's path in the page tree. Q1 = A2 is untouched —contentandbodystill do not joinPAGE_COMPONENT_COPY_KEYS, and nopackages/specschema changes.Authored by Claude Code in session
session_01ARYe3yQTQCUFm5qPYNgKaJ, stated here in prose because the platform normalises the session-URL footer down to the bare form on every edit.The defect, and why it had two faces
I18nLabelSchemaauthorizes two forms of a display label: a plain string whose translations live in a bundle, and an inline locale map written out at the authoring site. The walk read only the first.inlineText()narrowed a map toundefined— which is also what an absent prop produces — so one value carried two opposite facts from the very first line of the walk, and everything downstream inherited the confusion:missing translation— about text sitting in the file.Mechanism — how the gate now sees the map, with no node-path key
Nothing is addressed by position, because no new address was needed. Every one of these props already has a translation key (
objects.member.fields.email.help,pages.member_directory.label, …); what the walk lacked was evidence that the author had written anything at it. So the fix adds a third axis to an expected entry besidesourceValueandinline—inlineLocales, the map the author wrote, carried verbatim from the authoring site to the detector — and the threepush*helpers become the one place that reads it, so every call site inherits the behaviour instead of each re-deciding it. The detector then answers per locale from two sources rather than one: the bundle first, then the map's own entry for that locale. A key stays exactly where it always was, no bundle row is scaffolded, no key family is added, and an array index never becomes an address.The narrowing that makes this a real measurement rather than a blanket pass: only the tag-matching limbs of the shared
resolveI18nLabelrule count as coverage (exact tag, base language, region-qualified sibling of the same base). Its three fallback limbs — the untaggeddefaultentry,en, then any string in the map — are excluded on purpose, because falling back is what an untranslated locale looks like; reading coverage off the renderer's full chain would report every locale as covered the moment a map exists, which is the same one-answer-for-two-facts bug wearing the other hat.Recognising a map is delegated to
InlineLocaleMapSchemarather than pattern-matched here, so the retired{ key, defaultValue }key-reference dialect that schema refuses by name is not laundered into "authored" by a second, drifting predicate.Red first, then green — and the true report still fires
packages/cli/test/i18n-inline-locale-map-coverage.test.ts, run againstorigin/main's copies of the two changed source files (restored withgit checkoutfrom the merge base, both confirmed byte-identical to theorigin/mainblobs before the run, restored after, working tree verified clean):Three assertions, verbatim from the red run:
objects.member.fields.email.help, a map holdingen/zh-CN/ja-JPwith anenbundle row:expected [ 'ja-JP', 'zh-CN' ] to deeply equal []. Two locales reported missing while written out in full.en/zh-CN, asked aboutja-JP:expected [] to deeply equal [ 'ja-JP' ]. Today's gate says nothing about a locale that is genuinely absent; after the change it reports it.x.label ?? fallback, which is truthy for a map, so the seed handed to the bundle writer was the map object:seed was object. A view whose label is a map wrote a nested locale record into the extracted bundle where a translator expects a string. Same defect class, one expression over; the seed now narrows throughinlineTextfirst and the pin holds it to a string or nothing.The second of those is the answer to "is this just the check switched off": the change moves the verdict in both directions. And the five pins that pass on both legs are exactly the controls — an untranslated plain string still missing in every other locale, an empty map still not authoring, the retired key-reference dialect still refused, no bundle row scaffolded for a map in any locale, and every emitted key still free of a numeric path segment.
Verification
Union re-run at head
0691987f3(after mergingorigin/main), exit codes captured before any pipe:pnpm --filter @objectstack/cli exec vitest run --maxWorkers=2 test/i18n test/platform-page-i18n-parity.test.ts test/lint-platform-fold.test.ts test/unbuilt-workspace-lead.test.tspnpm --filter @objectstack/cli typecheckpnpm --filter @objectstack/cli buildpnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generatedpnpm check:i18n-coverage13 config(s), 621 baselined untranslated string(s), none newpnpm check:i18n9 package(s) — all bundles in syncpnpm check:i18n-walk-paritypnpm check:i18n-stale-fillpnpm check:app-nav-i18npnpm check:cross-package-test-inputspnpm check:test-source-aliaspnpm check:cli-test-child-envpnpm check:changeset-gate-self-testspnpm check:nul-bytespnpm lint(whole repo,eslint . --no-inline-config)The ratchet did not move: 621 before, 621 after. That is the measured confirmation of the projection recorded on the card — no config in this gate's population declares a page carrying an inline map, so the counting change has nothing to count there. On a project that does author them, it changes in both directions.
Published docs, read per file: nothing falsified
The drift checker reports that this diff yields no anchor for
packages/spec/src/ui/i18n.zod.tsand names its own structural blind spot — a page stating a rule by its INPUTS shares no identifier with the EMITTER implementing it, and this is an emitter behaviour change. So the question was answered by reading rather than by an anchor.content/docs/ui/translations.mdxandcontent/docs/protocol/kernel/i18n-standard.mdx— the two hand-written pages that document i18n coverage andos i18n extract— were read end to end. Neither states that an inline map is reported as untranslated, that only plain-string labels count toward coverage, or thatos i18n extractscaffolds a bundle key for a map; neither carries a worked example whose coverage numbers move (the CRM example uses plain-string labels throughout, andi18n-standard's450/450output block is an illustrative sample of a hypothetical stack, not a measured example from this repo). Nothing to fix under E3.Positive control for that null, on the same command and scope: the phrase "inline locale map" fires on 12 files under
content/docs/, and 0 of the 12 are outsidecontent/docs/references/. Every occurrence is in the auto-generated tree, is a schema.describe()about rendering ("resolved at render time") or about bundle addressing ("no translation-bundle slot addresses this key"), and remains true — this change adds no bundle slot.check:docsconfirms that tree is in sync, and the docblock edited here does not feed it (0 hits for its new prose in the generated output).content/docs/releases/v15/v16/v17mentionos i18n extractandI18nLabelhistorically, about other keys; none is falsified. ⛔ Not edited either way — release pages are never touched in a code PR.The one thing the reading did find is a gap, not a falsehood, and it is filed as #15984: the translations guide never mentions inline locale maps at all, which matters more after this PR because an author writing a partial map now gets a real lint finding the guide gives them no vocabulary for. Same class as #15435, which was accepted and fixed.
Clause-2 declaration, re-derived from the diff: no
Nothing in
packages/specchanges shape — the only edit there is 31 lines inside a docblock, andcheck:api-surface,check:authorable-surfaceandcheck:docsall report their artifacts unchanged.os i18n extractemits exactly what it emitted: a map-only entry carries nosourceValue, and the extractor's bundle build already filters on that, which the pin asserts per locale. No bundle key, no key family, no new authorable key. What changes is the diagnostic setos lintprints — one false finding removed, one real finding added — and a gate that stops lying widens no published contract face.Scope boundary, stated because it is easy to read this PR as wider than it is
This addresses the fourth of the four layers the measurement on this card identified. The other three are untouched and are not this card's: the three platform record pages are in no extract config (#14817), the walk's roots are
regions[].components[]while those pages author underslots.*, andcontent/bodyhave no bundle key family (settled as A2). The stale "31" ini18n.zod.tsis #14816 and is deliberately left as it is.